通过实战篇:导航预加载可知,在一个
Service Worker尚未启动的页面中,由于浏览器会等到 Service Worker 启动后才发起导航请求,且 Service Worker 启动可能会存在不同程度的延迟,该延迟将直接导致导航请求的延迟,进而增加了页面的整体渲染时间。为解决该问题,我们可以通过导航预加载机制让 Service Worker 的启动与导航请求并行执行,从而避免因 Service Worker 启动延迟而导致的页面渲染缓慢问题。由于我们已对相关底层 API 的使用进行了详细说明,故本章不再重述,而是直接讨论 Workbox 下导航预加载的使用。
# 基本使用
首先我们需要调用以下方法来启用导航预加载功能:
workbox.navigationPreload.enable();
然后我们可通过
workbox.routing.registerNavigationRoute方法注册导航请求路由:
workbox.routing.registerNavigationRoute(
workbox.precaching.getCacheKeyForURL('/single-page-app.html')
);
上述代码的效果是:当用户访问站点时,将使用预缓存资源
/single-page-app.html来响应所有的导航请求,由于方法workbox.routing.registerNavigationRoute默认使用预缓存资源进行响应,如果想要自定义响应缓存的来源,可通过以下方式实现:
workbox.routing.registerNavigationRoute(
'custom-cache-key',
{
cacheName: 'custom-cache-name'
}
);
有时我们可能只想要
/single-page-app.html来响应部分导航请求,此时可通过设置whitelist或blacklist属性来实现,比如:
workbox.routing.registerNavigationRoute(
workbox.precaching.getCacheKeyForURL('/single-page-app.html'),
{
whitelist: [
new RegExp('/blog/')
],
blacklist: [
new RegExp('/blog/restricted/')
]
}
);
通过配置,只有在满足导航请求路径以
/blog/开头且不以/blog/restricted/开头的情况下,才会使用缓存/single-page-app.html来响应该请求。
注:属性 whitelist 及 blacklist 的值为正则表达式数组。
使用方法
workbox.routing.registerNavigationRoute注册的导航请求路由采用的是缓存优先的请求策略,如果想要使用其他请求策略,可使用如下方式进行注册:
const strategy = new workbox.strategies.NetworkFirst(...);
const navigationRoute = new workbox.routing.NavigationRoute(strategy, {
whitelist: [],
blacklist: []
});
workbox.routing.registerRoute(navigationRoute);
也可使用自定义请求处理逻辑,比如:
const handlerCb = ({ url, event, request, params }) => {
return Promise.resolve(new Response(...));
};
const navigationRoute = new workbox.routing.NavigationRoute(handlerCb, {
whitelist: [],
blacklist: []
});
workbox.routing.registerRoute(navigationRoute);
# 综合运用
至此,我们完成了 Workbox 中预缓存、路由设置、请求策略、缓存置换策略及导航预加载的学习,下面我们将通过具体示例来看一下它们的综合运用(本示例代码仓库为:github.com/nanjingboy/…)。
首先我们需要使用 workbox-webpack-plugin 来动态生成预缓存资源列表:
// webpack.config.js
const { InjectManifest } = require('workbox-webpack-plugin');
module.exports = {
//... 其他配置
plugins: [
//... 其他插件
new InjectManifest({
swSrc: './client/sw.js',
swDest: 'sw.js',
importWorkboxFrom: 'local'
})
]
};
接下来,在 client/sw.js 中注册预缓存路由:
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
self.__precacheManifest的默认值来自通过第一步的 webpack 动态生成的precache-manifest.[hash].js文件,且在构建时自动生成引入该文件,故无需我们手动处理。
然后,在 client/sw.js中启动导航预加载并注册导航请求路由:
workbox.navigationPreload.enable();
const navigationRoute = new workbox.routing.NavigationRoute(workbox.streams.strategy([
({ url }) => fetchShell(url, 'top'),
({ event }) => fetchPageContent(event),
({ url }) => fetchShell(url, 'bottom')
]));
workbox.routing.registerRoute(navigationRoute);
示例中,我们使用了应用 Shell 架构,而使用
workbox.routing.registerNavigationRoute方法注册的导航路由并不适用于该架构,因此我们使用了上述较为繁琐的方式进行导航路由的注册。
在 workbox.routing.NavigationRoute 的构造函数中,我们调用了 workbox.streams.strategy 方法,并在其参数中,调用了函数 fetchShell 和 fetchPageContent,它们的主要实现如下:
async function fetchShell(url, type) {
const { pathname } = new URL(url, location);
let shellUrl;
if (pathname === '/') {
shellUrl = `/shell/home_${type}.html`;
} else if (/^\/create|\/edit\/\d+$/.test(pathname)) {
shellUrl = `/shell/edit_${type}.html`;
} else if (/^\/detail\/\d+$/.test(pathname)) {
shellUrl = `/shell/detail_${type}.html`;
}
const cache = await caches.open(workbox.core.cacheNames.precache);
return await cache.match(workbox.precaching.getCacheKeyForURL(shellUrl));
}
async function fetchPageContent(event) {
const cacheName = 'page-content';
try {
const { request: { url } } = event;
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
const clonePreloadResponse = preloadResponse.clone();
event.waitUntil((async () => {
const cache = await caches.open(cacheName);
await cache.put(url, clonePreloadResponse);
})());
return preloadResponse;
}
} catch {
}
const networkFirst = new workbox.strategies.NetworkFirst({
cacheName,
plugins: [
new workbox.expiration.Plugin({
maxAgeSeconds: 24 * 60 * 60
})
],
fetchOptions: {
headers: {
'only_content': 1
}
}
})
return await networkFirst.handle({ event });
}
在 fetchShell 中:
- 首先根据请求 URL 得到相关 Shell 文件 的路径;
- 然后通过
workbox.core.cacheNames.precache获得预缓存名,并调用caches.open打开相关缓存并获得相关实例 cache; - 最后通过
workbox.precaching.getCacheKeyForURL获得指定资源的cache key,并调用cache.match获取相关资源并返回。
在 fetchPageContent 中:
- 首先尝试从导航预加载请求中获得响应,如果请求成功便缓存并返回相关响应;
- 如果无法从导航预加载请求中获得响应,则使用网络优先策略来获得相关响应。
- 在 fetchPageContent 中,我们使用了模块
workbox.strategies和workbox.expiration,且我们已经在基本配置中讨论过使用workbox.*模块时需注意模块异步加载的问题,因此需要在 Service Worker 脚本的全局作用域中调用以下方法来避免相关问题:
workbox.loadModule('workbox-strategies');
workbox.loadModule('workbox-expiration');
# workbox.streams
上文中,我们提到了
workbox.streams,该模块是对ReadableStream的封装,主要有以下方法:
isSupported:用来判断当前浏览器是否支持ReadableStream。concatenate:该方法通过ReadableStream来处理所接收的Promise<Response|ReadableStream|BodyInit>数组,并返回结构为{done: Promise, stream: ReadableStream}的对象。concatenateToResponse:该方法是对concatenate的进一步封装,它将concatenate的返回值转换成结构为{done: Promise, response: Response}的对象。strategy:该方法是对concatenateToResponse的进一步封装,它接收签名为:({ url, request, event, params }) => Response|ReadableStream|BodyInit|Promise<Response|ReadableStream|BodyInit>的函数数组,并返回一个签名为:({ url, request, event, params }) => Promise<Response>的函数
concatenateToResponse和strategy方法均可设置headers信息(通过方法的最后一次参数),默认值为:
{ 'Content-Type': 'text/html' }
在实际应用中,我们应优先使用 strategy 方法,这是因为:
- 该方法可直接作为
workbox路由的handler参数。 - 如果浏览器不支持
ReadableStream,无需我们做任何判断,该方法将自动降级使用Promise.all。
# 总结
本章我们首先对 Workbox 中导航预加载的使用进行了简单介绍,接下来通过一个示例对前面章节中的预缓存、路由设置、请求策略、缓存置换策略及导航预加载进行了复习,最后我们对模块
workbox.streams进行了简单介绍。下一章,我们将对可缓存对象进行讨论。